5

至此,我们介绍了linux系统中常用命令的使用方法,简述了bash程序的使用方法和工作流程。在使用bash编写脚本程序时,熟练掌握这些工具的用法,往往能够达到事半功倍的效果。

本文将通过讲述一些实例,试着探讨bash脚本编程的技巧。需要说明的是,这里的技巧是多角度寻求解决方案的思路,是建立在对各种命令和bash编程技法深刻理解的基础之上的。

1、笔试题

先来看某公司的两个笔试题:

1、写脚本实现,可以用shell、perl等。在目录/tmp下找到100个以abc开头的文件,然后把这些文件的第一行保存到文件new中。

分析:寻找名字符合某个模式的文件可以用find,但find不能控制寻找到的文件数量,也许可以用for循环控制一下,查看文件的第一行有许多方法,可以用headsed等。

根据以上思路写出脚本:

#!/bin/bash
for name in `find /tmp -type f ! -empty -name 'abc*'`
do
    head -1 $name >> new && ((i++))
    [[ $i -eq 100 ]] && break
done

脚本中每次成功写入文件new中一行内容就令变量i自增,当i增长到100时,立即结束循环。

另一种方案:

#!/bin/bash
sed 100q <(head -qn1 $(find /tmp -type f -name 'abc*')) >new

此方案只有一条命令,也不难理解:$(find ...)部分获得文件名列表,<(head ...)部分获取每个文件的第一行(<(...)的用法请看这里),最后sed 100q ... >new取前100行写入文件new。

2、写脚本实现,可以用shell、perl等。把文件b中有的,但是文件a中没有的所有行,保存为文件c,并统计c的行数。

问题没什么可分析的,直接的解决方案:

#!/bin/bash
cat b|while read line
do
    if ! grep -xq $line a;then
        echo $line >>c
    fi
done
wc -l c

脚本通过循环读取文件b中的每一行,判断该行,如果该行不属于文件a,则输出该行内容到文件c中,循环结束后用wc统计文件c的行数。

另一种方案:

#!/bin/bash
grep -vxf a b|tee c|wc -l

此方案利用grep-f选项将文件a中的每行最为匹配模式匹配文件b的内容,-v表示不匹配,然后通过管道交给命令tee写入文件c中,然后在通过管道将标准输出交给wc命令统计行数。

2、清空日志

在使用linux服务器的过程当中,随着服务的长时间运行,有时会有删除服务日志的需求。由于日志文件正在被该服务所使用,并不能直接进行删除(准确说是:即使直接删除了,空间也没有得到释放,需要将服务重启),比较好的做法是利用重定向清空该文件(如:>some.log),既释放了空间,也不用重启服务。

但当需要清空的文件较多时,手动一个一个清空文件也有许多不方便,不如将需求写成脚本。

方案1:

#!/bin/bash
for log in `find /logs -name 'access_*.log'`
do
    >$i
done

脚本很容易理解,也可以用命令find /logs -name 'access_*.log' -exec cp /dev/null {} \;

但仍是一个文件执行一次,能不能一次性执行完呢?

方案2:

#!/bin/bash
find /logs -name 'access_*.log'|xargs tee

此方案巧妙的利用了命令xargstee将find找到的文件一次性清空。

3、分发

假设要对一个较大文件分别给不同的程序处理,并收集处理结果。
通常的处理的办法可能是串行的处理该文件,但如果各个程序需要较长的处理时间,串行处理将不能有效的利用机器的性能,如果不同的处理程序在后台并发运行,类似这样:cat file|command1 &cat file|command2 &cat file|command3 &...

这样处理能充分发挥服务器性能,但它的一个问题是,如果文件较大,对内存的消耗也会很大。

一种解决方案是:

#!/bin/bash
cat file|tee >(command1) >(command2) ... > >(commandN)|cat

此种方案的一个问题是,多个处理结果是随机的,如果需要处理结果是有序的(比如按命令的顺序输出),则不能满足需求。
另外,tee命令分发的速率是恒定的,所以只能按处理命令中最慢的速率分发,它们的输出将争用同一个管道,一定条件下,有可能造成死锁。

另一种解决方案:

#!/bin/bash
#定义处理函数
f() {
  mkfifo p{i,o}{1,2,3}
  command1 < pi1 > po1 &
  command2 < pi2 > po2 &
  command3 < pi3 > po3 &

  tee pi{1,2} > pi3 &
  cat po{1,2,3}
  rm -f p{i,o}{1,2,3}
}
cat file | f

此方案利用命名管道(见这里)处理分发及汇总各命令的输出,然后通过cat依次读取处理后的结果。

4、并发

我们在描述重定向与管道的文章中讲述过一种并发方式,下面介绍另一种。

我们说过,命令替换的问题是命令的立即执行然后等待结果,此时shell无法传入输入。bash使用一个称为进程替换的功能来弥补这些不足,进程替换实际上是命令替换管道的组合,和命令替换类似,bash运行一个命令,但令其运行于后台而不再等待其完成。关键在于Bash为这条命令打开了一个用于读和写的管道,并且绑定到一个文件名,最后展开为结果。

利用进程替换的这一特性,可以想到另外一种并发的方式:

#!/bin/bash
#处理函数,假设该函数的处理结果有且只有一个值
sth_todo() {
    #需要对第一个参数处理的命令
    some_command $1
}
#文件数组,也可以是其他待处理数据
file_list=('<(sth_todo '{1..10}.log')') #展开形如:<(sth_todo 1.log) <(sth_todo 2.log) <(sth_todo 3.log) ...
#收集结果并赋值给数组
read -a result <<<$(eval cat "${file_list[@]}")
#输出
echo "${result[@]}"

脚本中需要注意的地方在于数组的赋值和eval的运用(见这里)。

当然处理函数并不一定只有唯一结果,如有其他结果,只需将收集结果部分做相应更改即可。

5、数组交、并、差集

假定有需要取两个数组的交集(或并集、差集),简单的做法无非是两个循环对比两个数组中的每个值,取得相同的部分:

#!/bin/bash
list_1=(...)
list_2=(...)
for i in ${list_1[@]}
do
    for j in ${list_2[@]}
    do
        [[ "$i" == "$j" ]] && echo $i
    done
done

再看另一种方案:

list_1=(...)
list_2=(...)
#重置IFS值
IFS=$'\n'
#交集
grep -xf <(echo "${list_1[*]}") <<<"${list_2[*]}"
#并集
sort -u <<EOF
${ip[*]}
${ip_[*]}
EOF
#差集之一
grep -vxf <(echo "${list_1[*]}") <<<"${list_2[*]}"
#还原IFS
IFS=$' \t\n'

bash的一些特性和常用命令结合使用,使原本需要许多循环代码解决的问题变得“轻而易举”。但本例中,需要重点理解的是:IFS在数组扩展中的特性,命令grepsort的运用,以及进程替换的使用。

6、大量数据处理

假如需要对大量小文件进行简单的文本替换,而文件量已达到不可一次性处理的程度(比如几百万个)。
此时如果采用一般的处理办法,例如

find . -name '*.html' -exec sed -i 's/xxxx/oooo/g' {} \;

或类似的命令,显然,这样一个文件接着一个文件串行处理将花费巨大的时间成本。

对于此类问题,需要在服务器性能和时间成本上做取舍,先给出处理方案:

#!/bin/bash
#取得待处理文件数组
A=($(find . -name '*.html'))
#每10000个文件后台循环处理
for((i=0;i<${#A[@]};i+=10000))
do
    sed -i 's@xxxx@oooo@g' ${A[@]:$i:10000} &
done
#等待所有进程退出
wait

此方案假定服务器资源可以随意使用,只为达到时间效率最大化。如果需要控制服务器资源消耗(主要是IO性能),可以结合这一篇,控制并发的进程数量。

关于bash的文章,至此就告一段落了。

我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/dev...


vvpale
3.2k 声望252 粉丝

道可道,非常道。为学而不得不道。